//==============================================================================
//
//       DArguments - the program arguments parser in the ofc-library
//
//               Copyright (C) 2003  Dick van Oudheusden
//  
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
//==============================================================================
// 
//  $Date: 2004-02-12 19:59:20 $ $Revision: 1.11 $
//
//==============================================================================

#include "ofc/DArguments.h"

#include "ofc/warning.h"

#include "ofc/DText.h"
#include "ofc/DBool.h"

#include "ofc/DParsable.h"


//-// Private class

@interface DOption : Object
{
@private
  DText *_longOption;    // the long option
  char   _shortOption;   // the short option
  DText *_longDescr;     // the long option description
  DText *_description;   // the help text for the option
  id     _target;        // the target object
  BOOL   _hasParameter;  // needs the option a parameter ?
}

- (DOption *) init;
- free;
- (char) shortOption;
- (const char *) longOption;
- (const char *) description;
- (id) target;
- (BOOL) hasParameter;
- (DOption *) set :(const char *) longOption :(char) shortOption :(const char *) description :(id) target;
- (BOOL) checkLongOption :(char *) argument :(int) length;
- (BOOL) checkShortOption :(char) argument;

@end


@implementation DOption

//
// Initialise an empty option
// 
// @return the object
// 

- (DOption *) init
{
  [super init];
  
  _longOption   = nil;

  _longDescr    = nil;
  _description  = nil;
  
  _shortOption  = EOS;
  _target       = nil;
  
  return self;
}

//
// Deepen the object
// 
// @return the object
// 

- deepen
{
  [super deepen];

  if (_longOption != nil)
    _longOption = [_longOption copy];
  
  if (_longDescr != nil)
    _longDescr = [_longDescr copy];
  
  if (_description != nil)
    _description = [_description copy];
  
  return self;
}
  
//
// Free the option
// 
// @return the object
// 

- free
{
  if (_longOption != nil)
    [_longOption free];
  
  if (_longDescr != nil)
    [_longDescr free];
  
  if (_description != nil)
    [_description free];
  
  [super free];
  
  return self;
}


//// Member methods

//
// Rturn the short option
// 
// @return the short option
// 

- (char) shortOption
{
  return _shortOption;
}

//
// Return the long option
// 
// @return the long option
// 

- (const char *) longOption
{
  return (_longDescr != nil) ? [_longDescr cstring] : NULL;
}

//
// Return the description
// 
// @return the description
// 

- (const char *) description
{
  return (_description != nil) ? [_description cstring] : NULL;
}

//
// Check if the option has a parameter
// 
// @return has it ?
// 

- (BOOL) hasParameter
{
  return _hasParameter;
}

//
// Return the target object
// 
// @return the target
// 

- (id) target
{
  return _target;
}


//// Main methods

//
// Set the option
// 
// @param longOption  the long option with the parameter
// @param shortOption the short option
// @param description the help text
// @param target      the target object
// 
// @return the object
// 

- (DOption *) set :(const char *) longOption :(char) shortOption :(const char *) description :(id) target
{
  if (longOption != NULL)
  {
    long index;
    
    _longDescr  = [[DText alloc] init];
    _longOption = [[DText alloc] init];
    
    [_longDescr set :longOption];

    index = [_longDescr index :"=" :0 :-1];
    if (index < 1)
      index = [_longDescr index :":" :0 :-1];
  
    _hasParameter = (index > 0);
  
    if (index > 0)
    {
      [_longOption set :longOption :0 :index-1];
    }
    else
    {
      [_longOption set :longOption];
    }
  }
  
  if (description != NULL)
  {
    _description = [DText alloc];
    
    [_description init :description];
  }
  
  _shortOption = shortOption;
  _target      = target;
  
  return self;
}

//
// Check the long option in the argument
// 
// @param argument  the argument from the program
// @param length    the lenght of the argument
// 
// @return has the argument the option ?
// 

- (BOOL) checkLongOption :(char *) argument :(int) length
{
  return ((_longOption != nil) && ([_longOption length] == length) && ([_longOption ccompare :argument :length] == 0));
}

//
// Check the long option in the argument
// 
// @param argument  the argument from the program
// 
// @return has the argument the option ?
// 

- (BOOL) checkShortOption :(char) argument
{
  return (argument == _shortOption);
}

@end

  



//+// Public class

///
/// #include "ofc/config.h"
///
/// #include "ofc/DList.h"
///
/// // Structure for adding a group of program options
///
/// typedef struct
/// {
///   char           *longOption;     // the long option incl. parameter (date or date=DATE)
///   char            shortOption;    // the short option (d)
///   char           *description;    // the help text
///   id              target;         // the target object
/// } DArgOption;
///
/// // The DArguments class implements methods for parsing the arguments of a
/// // program. After adding the definition of the arguments, the arguments
/// // are parsed. The results are stored in boolean objects, parsable objects and a list of 
/// // texts. <br>
/// // Some notes: <br><ul>
/// // <li>Default options: -?/--help (show the help text) --version (show the version) -- 
/// // (stop parsing)
/// // <li>The target object for switches (options without parameter) must be the class DBool
/// // (or inherite from DBool). The target objects for parameter options must comply to
/// // the protocol DParsable.
/// // <li>The help format looks like the help text from the gnu tools (with column width=80)
/// // <li>The parameter for an option is required; optional parameters are not supported.
/// // <ul>
///
/// @interface DArguments : Object
/// {
/// @private
///   DList *_options;      // the options
///   int    _longColumn;   // the lenght of column with long options
/// }
///


@implementation DArguments


static const char INVALID_LONG_OPTION[]  = "%s: invalid option -- %s\n";
static const char INVALID_SHORT_OPTION[] = "%s: invalid option -- %c\n";
static const char LONG_ARG_REQUIRED[]    = "%s: option requires an argument: %s\n";
static const char SHORT_ARG_REQUIRED[]   = "%s: option requires an argument: %c\n";
static const char LONG_INVALID_ARG[]     = "%s: invalid argument for option: %s\n";
static const char SHORT_INVALID_ARG[]    = "%s: invalid argument for option: %c\n";
static const char OPTION_EXPECTED[]      = "%s: option expected -- %s\n";

//// Constructors

//
// Initialise an empty argument parser
//
// @return the object
//

- (DArguments *) init
{
  [super init];

  _options = [[DList alloc] init];
  
  _longColumn   = 12;  // option --version
  
  return self;
}


//// Copy related methods

//
// Deepen a copy of the parser (not implemented)
//
// @return the object
//

- deepen
{
  [super deepen];
  
  WARNING(DW_METHOD_NOT_IMPL, "deepen");
  
  return self;
}


//// Destructor

//
// Free the parser
//
// @return the object
//

- free
{
  [_options free];
  
  [super free];
  
  return self;
}


//// Option definition methods

//
// Add an option
// 
// @param longOption  the long option description (date or date=DATE)
// @param shortOption the short option description (d)
// @param description the help text for the option
//
// @return the object
//

- (DArguments *) option :(const char *) longOption
                        :(char)         shortOption 
                        :(const char *) description 
                        :(id)           target
{
  if (target == nil)
  {
    WARNING(DW_INVALID_ARG, "target");
  }
  else if ((shortOption == EOS) && (longOption == NULL))
  {
    WARNING(DW_INVALID_ARG, "shortOption/longOption");
  }
  else if ((longOption != NULL) && ((strlen(longOption) == 0) || (longOption[0] == ':') || (longOption[0] == '=')))
  {
    WARNING(DW_INVALID_ARG, "longOption");
  }
  else
  {
    DOption *option = [[DOption alloc] init];

    [option set :longOption :shortOption :description :target];
    
    if ([option hasParameter])
    {
      if (![target conformsTo :@protocol(DParsable)])
      {
        WARNING(DW_INVALID_PROT, "target");
      }
    }
    else
    {
      if (![target isKindOf :[DBool class]])
      {
        WARNING(DW_INVALID_CLASS, "target");
      }
    }

    [_options append :option];
    
    if (longOption != NULL)
    {
      int length = strlen(longOption) + 5;

      if (length > _longColumn)
      {
        _longColumn = length;
      }
    }
  }
  
  return self;
}

//
// Add an array of options
//  
// @param options   the array with options
// @param number    the number of elements in options
//
// @return the object
//

- (DArguments *) options :(const DArgOption *) options :(int) number
{
  if ((options != NULL) && (number > 0))
  {
    int i;
    
    for (i = 0; i < number; i++)
    {
      [self option :options[i].longOption :options[i].shortOption :options[i].description :options[i].target];
    }
  }
  
  return self;
}


//// Main method

//
// Parse the program arguments, leave the result in the target objects and the non-option list
//
// @param name     the program name
// @param usage    the usage text (or NULL)
// @param version  the version text (or NULL)
// @param tail     the tail text (or NULL)
// @param argv     the arguments array
// @param argc     the number of elements in argv
//
// @return the (new) list with all the non-option arguments (or nil if not found)
//

- (DList *) parse :(const char *) name 
                  :(const char *) usage 
                  :(const char *) version 
                  :(const char *) tail
                  :(char **)      argv 
                  :(int)          argc
{
  DList *nonOptions = nil;
  BOOL   skip       = NO;
  int    i          = 1;  // skip the program name
  
  if (name == NULL)
  {
    WARNING(DW_INVALID_ARG, "name");
    
    return nil;
  }
  
  while (i < argc)
  {
    if ((argv[i][0] == '-') && (!skip))
    {
      if (argv[i][1] == '-')
      {
        if (argv[i][2] == EOS)
        {
          skip = YES;
        }
        else
        {
          char *arg       = &argv[i][2];
          int   offset    = 0;

          while ((arg[offset] != '=') && (arg[offset] != ':') && (arg[offset] != EOS))
          {
            offset++;
          }
          
          if ((offset == 4) && (strncmp(arg, "help", offset) == 0))
            [self printHelp :usage :tail];
          else if ((offset == 7) && (strncmp(arg, "version", offset) == 0))
            [self printVersion :version];
          else
          {
            DOption       *option = nil;
            DListIterator *iter   = [DListIterator alloc];
            
            [iter init :_options];
              
            option = [iter first];
            while (option != nil)
            {
              if ([option checkLongOption :arg :offset])
                break;
              else
                option = [iter next];
            }
            [iter free];
              
            if (option == nil)
            {
              fprintf(stderr, INVALID_LONG_OPTION, name, arg);
            }
            else
            {
              if ([option hasParameter])
              {
                if (arg[offset] == EOS)
                {
                  fprintf(stderr, LONG_ARG_REQUIRED, name, arg);
                }
                else 
                {
                  char *param = arg+(offset+1);
                  if ([[option target] fromString :&param] != 0)
                  {
                    fprintf(stderr, LONG_INVALID_ARG, name, arg);
                  }
                }
              }
              else
              {
                [(DBool *) [option target] set :YES];
              }
            }
          }
        }
      }
      else // short option
      {
        if (argv[i][1] == EOS)
        {
          fprintf(stderr, OPTION_EXPECTED, name, argv[i]);
        }
        else
        {
          int j = 1;
          
          while (argv[i][j] != EOS)
          {
            char arg = argv[i][j];
            
            if (arg == '?')
            {
              [self printHelp :usage :tail];
              j++;
            }
            else
            {
              DOption       *option = nil;
              DListIterator *iter   = [DListIterator alloc];
            
              [iter init :_options];
              
              option = [iter first];
              while (option != nil)
              {
                if ([option checkShortOption :arg])
                  break;
                else
                  option = [iter next];
              }
              [iter free];
              
              if (option == nil)
              {
                fprintf(stderr, INVALID_SHORT_OPTION, name, arg);
                j++;
              }
              else
              {
                if ([option hasParameter])
                {
                  j++;
               
                  if (argv[i][j] == EOS)
                  {
                    i++;
                  
                    if (i < argc)
                    {
                      char *src = argv[i];
                    
                      if ([[option target] fromString :&src] != 0)
                      {
                        fprintf(stderr, SHORT_INVALID_ARG, name, arg);
                      }
                    }
                    else
                    {
                      fprintf(stderr, SHORT_ARG_REQUIRED, name, arg);
                    }
                  }
                  else 
                  {
                    if ((argv[i][j] == '=') || (argv[i][j] == ':'))
                    {
                      j++;
                    }
                  
                    if (argv[i][j] != EOS)
                    {
                      char *src = &argv[i][j];
                  
                      if ([[option target] fromString :&src] != 0)
                      {
                        fprintf(stderr, SHORT_INVALID_ARG, name, arg);
                      }
                    }
                    else
                    {
                      fprintf(stderr, SHORT_ARG_REQUIRED, name, arg);
                    }
                  }
                  
                  break;
                }
                else
                {
                  [(DBool *) [option target] set :YES];
                
                  j++;
                }
              }
            }
          }
        }
      }
    }
    else
    {
      DText *arg = [DText alloc];
      
      [arg init :argv[i]];
      
      if (nonOptions == nil)
        nonOptions = [[DList alloc] init];
     
      [nonOptions append :arg];
    }
  
    i++;
  }
  
  return nonOptions;
}


//// Help methods

//
// Print the version on stdout
// 
// @param version  the version text
// 
// @return the object
// 

- (DArguments *) printVersion :(const char *) version
{
  if (version != NULL)
    fprintf(stdout, "%s\n", version);

  fprintf(stdout, "This is free software; see the source for copying conditions. There is NO\n");
  fprintf(stdout, "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n");
  
  return self;
}

//-//
//
// Fill spaces till a certain position
// 
// @param cur      the current position
// @param pos      the requested position
// 
// @return none
// 

static void fillSpaces(int cur, int pos)
{
  while (cur < pos)
  {
    fputc(' ', stdout);
    cur++;
  }
}
//+//

//
// Print the option help on stdout
// 
// @param shortOption   the short option (or EOS)
// @param longColumn    the length of the long column
// @param longOption    the long option (or NULL)
// @param description   the description (or NULL)
// 
// @return none
//

- (void) printOptionHelp :(char) shortOption
                         :(int) longColumn
                         :(const char *) longOption
                         :(const char *) description
{
  int pos;
  
  if (shortOption != EOS)
  {
    fprintf(stdout, "  -%c", shortOption);
  }
  else
  {
    fillSpaces(0, 4);
  }
  
  if ((shortOption != EOS) && (longOption != NULL))
  {
    fprintf(stdout, ", ");
  }
  else
  {
    fillSpaces(0, 2);
  }
  
  if (longOption != NULL)
  {
    pos = fprintf(stdout, "--%s", longOption);
  }
  else
  {
    pos = 0;
  }

  fillSpaces(pos, longColumn);
  
  if (description != NULL)
  {
    const char  *column;
    DText *descr = [DText alloc];
    
    [descr init   :description];
    [descr column :80 - longColumn - 6];

    column = [descr cstring];
    
    while (*column != EOS)
    {
      while ((*column != EOS) && (*column != '\n'))
      {
        fputc(*column++, stdout);
      }
      
      if (*column == '\n')
      {
        fputc(*column++, stdout);
       
        if (*column != EOS)
        {
          fillSpaces(0, (longColumn+6));
        }
      }
      else
        fputc('\n', stdout);
    }
    [descr free];
  }
  else
  {
    fputc('\n', stdout);
  }
}
                        
//
// Print the standard help on stdout
//
// @param usage    the usage text
// @param tail     the tail text
//
// @return the object
//

- (DArguments *) printHelp :(const char *) usage :(const char *) tail
{
  DListIterator *iter   = [DListIterator alloc];
  DOption       *option = nil;
  
  if (usage != NULL)
    fprintf(stdout, "%s\n\n", usage);

  [iter init :_options];

  [self printOptionHelp :'?' :_longColumn :"help"    :"show this help"];
  [self printOptionHelp :EOS :_longColumn :"version" :"show version info"];
  
  option = [iter first];
  while (option != nil)
  {
    [self printOptionHelp :[option shortOption]
                          :_longColumn 
                          :[option longOption]
                          :[option description]];

    option = [iter next];
  }
  [iter free];
  
  if (tail != NULL)
    fprintf(stdout, "\n%s\n", tail);
  
  return self;
}

@end

/*==========================================================================*/
